﻿namespace MefContrib.Models.Provider
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.Composition.Primitives;
    using System.Linq;

    /// <summary>
    /// 
    /// </summary>
    public class ProviderComposablePart : ComposablePart
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="ProviderComposablePart"/> class,
        /// using the specified definition.
        /// </summary>
        /// <param name="definition">The definitions to use when creating the composable part.</param>
        public ProviderComposablePart(ProviderComposablePartDefinition definition)
            : this(null, definition)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ProviderComposablePart"/> class,
        /// for the specified object instance.
        /// </summary>
        /// <param name="instance">The object instance to create the composable part for.</param>
        public ProviderComposablePart(object instance)
            : this(instance, null)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ProviderComposablePart"/> class,
        /// for the specified object instance, using the specified definition
        /// </summary>
        /// <param name="instance">The object instance to create the composable part for.</param>
        /// <param name="definition">The definitions to use when creating the composable part.</param>
        private ProviderComposablePart(object instance, ProviderComposablePartDefinition definition)
        {
            this.Definition = 
                definition ?? DefinitionLocator.GetDefinition(instance);

            this.ImportedValues = new Dictionary<ImportDefinition, object>();
            this.Instance = instance;
        }

        /// <summary>
        /// Gets the definition used to define the composable part.
        /// </summary>
        /// <value>A <see cref="ProviderComposablePartDefinition"/> object.</value>
        public ProviderComposablePartDefinition Definition { get; private set; }

        /// <summary>
        /// Gets a list of all known <see cref="ExportDefinition"/> objects for the part.
        /// </summary>
        /// <value>A <see cref="IEnumerable{ExportDefinition}"/> object.</value>
        public override IEnumerable<ExportDefinition> ExportDefinitions
        {
            get { return this.Definition.ExportDefinitions; }
        }

        /// <summary>
        /// Gets a list of all known <see cref="ImportDefinition"/> objects for the part.
        /// </summary>
        /// <value>A <see cref="IEnumerable{ImportDefinition}"/> object.</value>
        public override IEnumerable<ImportDefinition> ImportDefinitions
        {
            get { return this.Definition.ImportDefinitions; }
        }

        /// <summary>
        /// Gets or sets a dictionary of all the values for nested imports.
        /// </summary>
        /// <value>A <see cref="Dictionary{ImportableMember, Object}"/> object.</value>
        private Dictionary<ImportDefinition, object> ImportedValues { get; set; }

        /// <summary>
        /// Gets or sets the instance object of the <see cref="ProviderComposablePart"/>.
        /// </summary>
        /// <value>An <see cref="object"/> containing the instance.</value>
        /// <remarks>
        /// This value is either set from the constructor which accepts an object instance or from
        /// the <see cref="GetActivatedInstance"/> method.
        /// </remarks>
        private object Instance { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether or not if all the imports of the part has been
        /// composed by the composition engine.
        /// </summary>
        /// <value><see langword="true" /> if composition is done; otherwise <see langword="false" />.</value>
        private bool IsComposed { get; set; }

        /// <summary>
        /// Creates a new instance of the <see cref="ComposableMember"/>, specified by the
        /// <param name="exportable"></param> parameter, if it's needed.
        /// </summary>
        /// <param name="exportable">The exportable member to create an instance of, if it is required.</param>
        /// <returns>An instance of the object specified by the provided <see cref="ComposableMember"/>.</returns>
        private object GetActivatedInstance(ComposableMember exportable)
        {
            if (this.Instance != null)
            {
                return this.Instance;
            }

            this.Instance =
                this.Definition.Catalog.Factory.Create(exportable.DeclaringType);

            if (this.IsComposed)
            {
                this.SatisfyPostCompositionImports();
            }

            return this.Instance;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="definition"></param>
        /// <returns></returns>
        /// <exception cref="InvalidOperationException">The supplied export definition was of an unknown type.</exception>
        public override object GetExportedValue(ExportDefinition definition)
        {
            ProviderExportDefinition export =
                definition as ProviderExportDefinition;

            if (export == null)
            {
                throw new InvalidOperationException("The supplied export definition was of an unknown type.");
            }

            ComposableMember exportable =
                export.Member.ToComposableMember();

            object instance = null;

            if (exportable.IsInstanceNeeded)
            {
                instance = this.GetActivatedInstance(exportable);
            }

            object value =
                exportable.GetValue(instance);

            return value;
        }

        public override void Activate()
        {
            OnComposed();
        }

        /// <summary>
        /// 
        /// </summary>
        public void OnComposed()
        {
            if (this.Instance != null)
            {
                if (!IsComposed)
                {
                    SatisfyPostCompositionImports();
                }
            }

            IsComposed = true;
        }

        /// <summary>
        /// 
        /// </summary>
        private void SatisfyPostCompositionImports()
        {
            IEnumerable<ImportDefinition> members =
                this.ImportDefinitions.Where(import => !import.IsPrerequisite);

            foreach (ProviderImportDefinition definition in members)
            {
                object value;
                if (this.ImportedValues.TryGetValue(definition, out value))
                {
                    ComposableMember importable =
                        definition.Member.ToComposableMember();

                    importable.SetValue(this.Instance, value);
                }
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="definition"></param>
        /// <param name="exports"></param>
        /// <exception cref="InvalidOperationException">The supplied import definition was of an unknown type.</exception>
        public override void SetImport(ImportDefinition definition, IEnumerable<Export> exports)
        {
            ProviderImportDefinition import =
                definition as ProviderImportDefinition;

            if (import == null)
            {
                throw new InvalidOperationException("The supplied import definition was of an unknown type.");
            }

            ComposableMember importable =
                import.Member.ToComposableMember();

            object value =
                importable.GetImportValueFromExports(exports);

            this.ImportedValues[definition] = value;

            if (this.Instance != null && this.IsComposed)
            {
                if (value != null)
                {
                    importable.SetValue(this.Instance, value);
                }
            }
        }
    }
}